//go:generate goversioninfo -icon favicon.ico -manifest manifest.xml
package main

import (
	"bytes"
	"embed"
	"encoding/xml"
	"fmt"
	"io"
	"io/fs"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"syscall"
)

//go:embed assets/setup.*
var assets embed.FS

const (
	Setup    = "/configure"
	Download = "/download"
)

func init() {
	file, err := ioutil.TempFile(os.TempDir(), "install-*.log")
	if err != nil {
		panic(err)
	}
	//defer func() { _ = file.Close() }()

	log.SetOutput(file)
	log.SetPrefix("[OfficeSetup]")
	log.SetFlags(log.LstdFlags | log.Lshortfile | log.LUTC)
}

func main() {
	var err error
	var exePath, xmlPath string

	defer func() {
		_ = os.Remove(exePath)
		_ = os.Remove(xmlPath)

		if err := recover(); err != nil {
			log.Fatalln(err)
		}
	}()

	exePath, err = writeEmbedFileToTempFile("assets/setup.exe", "install-*.exe")
	if err != nil {
		panic(err)
	}

	// Golang always creates a random temporary file. Therefore, specifying the file name will not work.
	_, fileName := filepath.Split(exePath)
	fileName = strings.Join([]string{nameWithoutExtension(fileName), "xml"}, ".")

	//fullPath, _ = os.Executable()
	//workDir = filepath.Dir(fullPath)
	workDir, _ := os.Getwd()

	xmlPath, err = updateEmbedFileToTempFile("assets/setup.xml", "install-*.xml",
		func(element *xml.StartElement) {
			if element.Name.Local == "Add" {
				element.Attr = append(element.Attr,
					xml.Attr{Name: xml.Name{Local: "SourcePath"}, Value: workDir},
					xml.Attr{Name: xml.Name{Local: "AllowCdnFallback"}, Value: "true"},
				)
			}
		})
	if err != nil {
		panic(err)
	}

	if !PathExist(filepath.Join(workDir, "Office")) {
		if _, err = runCommandWithoutWindow(workDir, exePath, Download, xmlPath); err != nil {
			panic(err)
		}
	}
	if _, err = runCommandWithoutWindow(workDir, exePath, Setup, xmlPath); err != nil {
		panic(err)
	}
}

func PathExist(path string) bool {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return false
	}
	return true
}

func nameWithoutExtension(name string) string {
	return strings.TrimSuffix(name, path.Ext(name))
}

func runCommandWithoutWindow(dir, name string, arg ...string) (code int, err error) {
	cmd := exec.Command(name, arg...)
	// Hides the called CMD window.
	cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
	cmd.Dir = dir

	if err = cmd.Run(); err != nil {
		_, _ = fmt.Sscanf(err.Error(), "exit status %d", &code)
	}
	return code, err
}

// Create a temporary file and write embed content to it.
func writeEmbedFileToTempFile(embed, pattern string) (string, error) {
	var err error
	var file *os.File
	var bin []byte
	file, err = ioutil.TempFile(os.TempDir(), pattern)
	if err != nil {
		return "", err
	}
	defer func() { _ = file.Close() }()

	bin, err = assets.ReadFile(embed)
	if err == nil {
		_, err = file.Write(bin)
	}
	return file.Name(), err
}

func updateEmbedFileToTempFile(embed, pattern string, callback func(element *xml.StartElement)) (string, error) {
	var err error
	var bin fs.File
	var file *os.File

	bin, err = assets.Open(embed)
	if err != nil {
		return "", err
	}
	defer func() { _ = bin.Close() }()

	var buf bytes.Buffer
	decoder := xml.NewDecoder(bin)
	encoder := xml.NewEncoder(&buf)

	for {
		token, err := decoder.Token()
		if err == io.EOF {
			break
		}
		if err != nil {
			return "", err
		}

		switch element := token.(type) {
		case xml.StartElement:
			callback(&element)
			err = encoder.EncodeToken(element)
		default:
			err = encoder.EncodeToken(token)
		}

		if err != nil {
			return "", err
		}
	}

	// must call flush, otherwise some elements will be missing
	if err := encoder.Flush(); err != nil {
		return "", err
	}

	log.Println(buf.String())
	file, err = ioutil.TempFile(os.TempDir(), pattern)
	if err != nil {
		return "", err
	}
	defer func() { _ = file.Close() }()

	_, err = file.Write(buf.Bytes())

	return file.Name(), err
}
